package com.helloworld.upload.util;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cfw.exception.BizException;
import com.helloworld.upload.domain.CellInfo;
import com.helloworld.upload.domain.ColumnInfo;
import com.helloworld.upload.domain.UploadInfo;
import com.helloworld.upload.domain.ValidateRule;

/**
 * 엑셀데이터 검증을 수행하는 함수 제공
 * 
 * @author 박형일
 */
public class ValidateUtil {
	
	private static final Logger logger = LoggerFactory.getLogger(ValidateUtil.class);
	
		/**
		 * 엑셀셀로부터 값을 조회하여 반환
		 * @param cell
		 * @return
		 */
		/*
		public static Object getValue(Cell cell) {
			Object result = null;
			
			switch (cell.getCellType()) {
			case Cell.CELL_TYPE_STRING:
				result = cell.getRichStringCellValue().getString();
				break;
			case Cell.CELL_TYPE_NUMERIC:
				if (DateUtil.isCellDateFormatted(cell)) {
					result = cell.getDateCellValue();
				} else {
					result = cell.getNumericCellValue();
				}				
				break;
			case Cell.CELL_TYPE_BOOLEAN:
				result = cell.getBooleanCellValue();
				break;
			case Cell.CELL_TYPE_FORMULA:
				result = cell.getCellFormula();
				break;
			default:
				result = cell.getStringCellValue();
			}
			
			return result;			
		}
		*/
		
		/**
		 * 변환되어야 할 테이블의 컬럼타입에 따른 데이터 변환이 필요한 경우
		 * info.setAdjustValue()를 이용하여 설정
		 * @param info
		 * @param cell
		 */
		public static void setValue(CellInfo info, Cell cell) {
			switch (cell.getCellType()) {
			case Cell.CELL_TYPE_STRING:
				info.setValue(cell.getRichStringCellValue().getString());
				info.setAdjustValue(cell.getRichStringCellValue().getString());
				break;
			case Cell.CELL_TYPE_NUMERIC:
				if (DateUtil.isCellDateFormatted(cell)) {
					info.setValue(cell.getDateCellValue());
					info.setAdjustValue(cell.getDateCellValue());
				} else {
					info.setValue(cell.getNumericCellValue());
					info.setAdjustValue(cell.getNumericCellValue());
				}				
				break;
			case Cell.CELL_TYPE_BOOLEAN:
				info.setValue(cell.getBooleanCellValue());
				info.setAdjustValue(cell.getBooleanCellValue());
				break;
			case Cell.CELL_TYPE_FORMULA:
				info.setValue(cell.getCellFormula());
				info.setAdjustValue(cell.getCellFormula());
				break;
			default:
				info.setValue(cell.getStringCellValue());
				info.setAdjustValue(cell.getStringCellValue());
			}
		}
		
		/**
		 * 전달받은 double변수에 대한 오라클 number의 precision, scale을 산출
		 * @param d
		 * @return
		 */
		private static int[] getOrclePrecisionScale(double d) {
			int[] result = new int[2];
					
			String numberStr = new BigDecimal(Double.toString(d)).toPlainString();
			
			int pos = numberStr.indexOf('.');
			
			if (pos == -1) {
				result[0] = numberStr.length();
				result[1] = 0;
			} else {
				result[0] = numberStr.length() - 1;
				result[1] = numberStr.length() - pos - 1;
			}
			
			return result;
		}
			
		/**
		 * 데이터타입, 데이터길이에 대한 정합성 검증
		 * @param rule
		 * @param cell
		 * @return
		 */
		private static void validateTypeSize(CellInfo cellInfo, ValidateRule rule, Cell cell) {
			if (cell == null) { return; }
			
			switch (cell.getCellType()) {
			case Cell.CELL_TYPE_STRING:
				// 타입확인
				if (rule.getRuleFieldType() != ValidateRule.TypeSize.EnumFieldType.VARCHAR2) {
					cellInfo.addError(
							ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_KEY_INDEX], 
							String.format(ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_MSG_INDEX], 
									rule.getRuleFieldType().name(), "문자열")
					);
				}
				
				// 길이확인
				if (rule.getRuleItemSize() < cell.getRichStringCellValue().getString().getBytes().length) {
					cellInfo.addError(
							ValidateRule.Err.OverSize[ValidateRule.ERR_KEY_INDEX],
							String.format(ValidateRule.Err.OverSize[ValidateRule.ERR_MSG_INDEX], 
									rule.getRuleItemSize(), rule.getRuleItemScale(), cell.getRichStringCellValue().getString().getBytes().length, 0)
					);
				}
				break;
			case Cell.CELL_TYPE_NUMERIC:
				// 타입확인 - 룰이 일자, 타임스탬프인 경우
				if (rule.getRuleFieldType() == ValidateRule.TypeSize.EnumFieldType.DATE || rule.getRuleFieldType() == ValidateRule.TypeSize.EnumFieldType.TIMESTAMP) {
					if (!DateUtil.isCellDateFormatted(cell)) {
						cellInfo.addError(
								ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_KEY_INDEX], 
								String.format(ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_MSG_INDEX], 
										rule.getRuleFieldType().name(), "숫자")
						);
					}
				} else
				// 타입확인 - 툴이 숫자이고, 셀값이 날짜인 경우
				if (rule.getRuleFieldType() == ValidateRule.TypeSize.EnumFieldType.NUMBER) {
					if (DateUtil.isCellDateFormatted(cell)) {
						cellInfo.addError(
								ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_KEY_INDEX], 
								String.format(ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_MSG_INDEX], 
										rule.getRuleFieldType().name(), "날짜")
						);
					}
				} else
				// 타입확인 - 룰이 숫자가 아닌 경우
				if (rule.getRuleFieldType() != ValidateRule.TypeSize.EnumFieldType.NUMBER) {
					cellInfo.addError(
							ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_KEY_INDEX], 
							String.format(ValidateRule.Err.TypeNotMatch[ValidateRule.ERR_MSG_INDEX], 
									rule.getRuleFieldType().name(), "숫자")
					);
				}
				
				// 길이확인
				if (!DateUtil.isCellDateFormatted(cell)) { // 일자, 타임스탬프일 경우는 길이 확인 불필요
					int[] precisionScale = getOrclePrecisionScale(cell.getNumericCellValue());
					
					if (precisionScale[0] > rule.getRuleItemSize() || precisionScale[1] > rule.getRuleItemScale()) {
						cellInfo.addError(
								ValidateRule.Err.OverSize[ValidateRule.ERR_KEY_INDEX],
								String.format(ValidateRule.Err.OverSize[ValidateRule.ERR_MSG_INDEX],
										rule.getRuleItemSize(), rule.getRuleItemScale(), precisionScale[0], precisionScale[1])
						);
					}
				}
				break;
			case Cell.CELL_TYPE_BOOLEAN:
				cellInfo.addError(
						ValidateRule.Err.BooleanNotAllowed[ValidateRule.ERR_KEY_INDEX],
						ValidateRule.Err.BooleanNotAllowed[ValidateRule.ERR_MSG_INDEX]);
				break;
			case Cell.CELL_TYPE_FORMULA:
				cellInfo.addError(
						ValidateRule.Err.FormulaNotAllowed[ValidateRule.ERR_KEY_INDEX],
						ValidateRule.Err.FormulaNotAllowed[ValidateRule.ERR_MSG_INDEX]);
				break;
			}
		}
		
		/**
		 * 참조무결성 검증
		 * @param rule
		 * @param value
		 * @return
		 */
		private static void validateRi(CellInfo cellInfo, ValidateRule rule) {
			Map<String, Object> lookup = rule.getLookup();
			
			if (lookup == null) {
				throw new BizException("E00001");
			} else
			if (cellInfo.getValue() != null && !lookup.containsKey(cellInfo.getValue())) {
				cellInfo.addError(
						ValidateRule.Err.ReferenceNotFound[ValidateRule.ERR_KEY_INDEX],
						ValidateRule.Err.ReferenceNotFound[ValidateRule.ERR_MSG_INDEX]);
			}
		}
		
		/**
		 * 참조무결성 검증 및 대치 수행
		 * @param rule
		 * @param value
		 * @return
		 */
		private static void validateRiReplace(CellInfo cellInfo, ValidateRule rule) {
			Map<String, Object> lookup = rule.getLookup();
			
			if (lookup == null) {
				throw new BizException("E00001");
			} else
			if (cellInfo.getValue() != null) {
				if (!lookup.containsKey(cellInfo.getValue())) {
					cellInfo.addError(
							ValidateRule.Err.ReferenceNotFound[ValidateRule.ERR_KEY_INDEX],
							ValidateRule.Err.ReferenceNotFound[ValidateRule.ERR_MSG_INDEX]);
				} else {
					Object newValue = lookup.get(cellInfo.getValue());
					if (newValue == null) {
						cellInfo.addError(
								ValidateRule.Err.ReferenceIsNull[ValidateRule.ERR_KEY_INDEX],
								ValidateRule.Err.ReferenceIsNull[ValidateRule.ERR_MSG_INDEX]);					
					} else {
						cellInfo.setValue(newValue);
					}
				}
			}
		}		
		
	public static final String RESULT_KEY_ERROR = "error";
	public static final String RESULT_KEY_VALUE = "value";
	
	/**
	 * 개별셀에 대한 검증수행
	 * @param rules
	 * @param cell
	 * @return
	 */
	public static CellInfo validate(List<ValidateRule> rules, Cell cell, int rowIndex, int columnIndex) {
		CellInfo cellInfo = new CellInfo();
		
//		if (cell != null) { cellInfo.setValue(getValue(cell)); }
		if (cell != null) { setValue(cellInfo, cell); }
		cellInfo.setCellIndex(rowIndex, columnIndex);
		
		for (ValidateRule rule : rules) {
			switch (rule.getRule()) {
			case TypeSize:
				validateTypeSize(cellInfo, rule, cell);
				break;
			case RI:
				validateRi(cellInfo, rule);
				break;
			case RiReplace:
	 			validateRiReplace(cellInfo, rule);
				break;
			case Null:
				if (cell == null) {
					cellInfo.addError(
							ValidateRule.Err.ReferenceIsNull[ValidateRule.ERR_KEY_INDEX],
							ValidateRule.Err.ReferenceIsNull[ValidateRule.ERR_MSG_INDEX]);					
				}
				break;
			case Const:
				cellInfo.setValue(rule.getRuleItemValue());
				break;
			default:
				break;
			}
		}

		return cellInfo;
	}

	/**
	 * UplodaInfo에 포함된 컬럼정보에서 키컬럼인 엑셀소스컬럼 인덱스들을 조회
	 * @param uploadInfo
	 * @return
	 */
	public static List<Integer> getKeyColumnIndex(UploadInfo uploadInfo) {
		List<Integer> result = new ArrayList<Integer>();
	
		for(ColumnInfo info : uploadInfo.getColumns()) {
			if (info.isKeyColumn()) { result.add(info.getSourceColumnIndex()); }
		}
		
		return result;
	}
	
	public static List<Map<Integer, Object>> getDistincted(UploadInfo uploadInfo, Map<Integer, List<CellInfo>> rowInfos) {
		// Map<"value", Map<"value" ...> 형식의 트리구조로 데이터들을 distinct 처리
		Map<Object, Object> tree = new HashMap<Object, Object>();
		List<Integer> keys = getKeyColumnIndex(uploadInfo);
		List<Map<Integer, Object>> distincts = new ArrayList<Map<Integer, Object>>();
		
		for (int rowIndex : rowInfos.keySet()) {
//			logger.debug("*** row:" + rowIndex + ", rowInfo:" + rowInfos.get(rowIndex));
			
			// 키컬럼에 포함되는 데이터만 갖는 배열 생성
			// keys[i, j, ...]를 기준으로 values[컬럼인덱스가 i인 데이터, 컬럼인덱스가 j인 데이터) 생성
			Object[] values = new Object[keys.size()];
			for (int idx : keys) {
				for (CellInfo cellInfo : rowInfos.get(rowIndex)) {
					if (idx == cellInfo.getColumnIndex()) { values[keys.indexOf(idx)] = cellInfo.getValue(); }
				}
			}
			
//			String log = "";
			
			Map<Object, Object> ref = tree; // 행이동시 레퍼런스변수 초기화
			Map<Integer, Object> distinct = new HashMap<Integer, Object>();
			for (int i = 0; i < values.length; i++) {
				// 현재 노드에서 현재 레벨의 값이 트리에 이미 포함되어 있는지를 조회
				Map<Object, Object> search = (Map<Object, Object>)ref.get(values[i]);
//				logger.debug("+++ [" + rowIndex + "] tree.get(" + values[i] + "):" + tree.get(values[i]));
				distinct.put(keys.get(i), values[i]); // 현재 레벨의 값을 distinct에 추가
				if (search == null) {
					search = new HashMap<Object, Object>();
					ref.put(values[i], search);
					
//					logger.debug("*** added:" + values[i]);
					// 마지막 레벨의 노드가 조회되었다면 기존에 있던 자료, 조회되지 않았다면 목표데이터에 추가  
					if (i == values.length - 1) { 
//						logger.debug("*** distinct:" + distinct);
						distincts.add(distinct);
					}
				}
				ref = search;
				
//				log += (("".equals(log) ? "" : ",") + values[i]); 
			}
//			logger.debug("*** value:" + log);
//			logger.debug("*** tree:" + tree);
		}			
		return distincts;
	}	
}
